---
title: 'OpenAPI'
description: How to use ts-rest with OpenAPI
---

## Installation

`@ts-rest/open-api` is a library that allows you to generate OpenAPI documents from your ts-rest contracts, it's a great way to expose your API
to non-ts-rest clients - for example a public API or a client library.

<InstallTabs packageName="@ts-rest/open-api" />

## Generating an OpenAPI Document

```typescript
import { contract } from './contract';
import { generateOpenApi, SchemaTransformerAsync } from '@ts-rest/open-api';

const openApiDocument = generateOpenApi(
  contract,
  {
    info: {
      title: 'Posts API',
      version: '1.0.0',
    },
  },
  {
    schemaTransformer: MY_TRANSFORMER, // [!code highlight]
    // Bring your own transformer here
  },
);
```

That's it! Once you've picked your transformer, you can serve this document however you want, use it for CodeGen for your non-TS clients, or use it to generate a Swagger UI!

## Schema Transformers

ts-rest supports multiple schema validation libraries through schema transformers. These transformers convert your schema definitions into OpenAPI-compatible JSON schemas.

There are two flavors of transformers:

- **Sync transformers** (`SchemaTransformerSync`) - for libraries that can transform schemas synchronously
- **Async transformers** (`SchemaTransformerAsync`) - for libraries that require asynchronous transformation

<Callout type="warning">
  For backwards compatibility, ts-rest currently defaults to a built-in Zod 3
  transformer (`ZOD_3_SCHEMA_TRANSFORMER`). However, **this built-in support
  will be removed in v4**, and you should start providing your own transformer.
</Callout>

### Zod 3

#### Transformer Implementation

```typescript
import { SchemaTransformer } from './types';
import { generateSchema } from '@anatine/zod-openapi';
import { z } from 'zod';

export const ZOD_3_SYNC: SchemaTransformerSync = ({ schema }) => {
  if (schema instanceof z.ZodAny) {
    return generateSchema(schema);
  }
  return null;
};
```

#### Adding Metadata to Contracts

For Zod 3, you can extend your schemas with additional OpenAPI information using `@anatine/zod-openapi`. This allows you to add `title`, `description`, and `example` fields to improve the quality and clarity of the generated documentation.

First, extend Zod with `extendZodWithOpenApi(z)` when defining the schemas for your contract:

```ts
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
import { extendZodWithOpenApi } from '@anatine/zod-openapi';

extendZodWithOpenApi(z);

const c = initContract();

export const contract = c.router({
  getUser: {
    method: 'GET',
    path: '/users/:id',
    pathParams: z.object({
      id: z.string().openapi({
        description: "The user's ID",
      }),
    }),
    responses: {
      200: z.object({
        id: z.string().uuid().openapi({
          title: 'Unique ID',
          description: 'A UUID generated by the server',
        }),
        name: z.string(),
        phoneNumber: z.string().min(10).openapi({
          description: 'US phone numbers only',
          example: '555-555-5555',
        }),
      }),
    },
  },
});
```

See the [official `@anatine/zod-openapi` docs](https://www.npmjs.com/package/@anatine/zod-openapi) for more information.

<Callout type="info">
  All Zod schemas defined in your contract can benefit from the additional
  OpenAPI schema, including `pathParams`, `queryParams`, and `responses`. These
  keys are all used when generating the OpenAPI JSON file. This could improve
  the quality of your generated documentation.
</Callout>

#### Adding Examples to the Media Type

In order to add examples to the media type rather than to the schema itself (this is useful if you want to show multiple examples), we have added a `mediaExamples` property to the `.openapi()` method options.
This will only work for the schemas of the body, responses and individual query parameters if you are using `jsonQuery` option.

You can see an example of its usage in the code snippet above.

### Zod 4

#### Transformer Implementation

```typescript
import { z } from 'zod/v4';
import { SchemaTransformerAsync } from './types';
import convert from '@openapi-contrib/json-schema-to-openapi-schema';

export const ZOD_4_ASYNC: SchemaTransformerAsync = async ({ schema }) => {
  if (schema instanceof z.core.$ZodAny) {
    const jsonSchema = z.toJSONSchema(schema);

    return await convert(jsonSchema);
  }

  return null;
};
```

### Valibot

#### Transformer Implementation

```typescript
import { isStandardSchema } from '@ts-rest/core';
import type { SchemaTransformerAsync } from '@ts-rest/open-api';
import { toJsonSchema } from '@valibot/to-json-schema';
import { convert } from '@openapi-contrib/json-schema-to-openapi-schema';

export const VALIBOT_ASYNC: SchemaTransformerAsync = async ({ schema }) => {
  if (isStandardSchema(schema) && schema['~standard'].vendor === 'valibot') {
    const jsonSchema = toJsonSchema(schema as any, { errorMode: 'ignore' });
    return await convert(jsonSchema);
  }

  return null;
};
```

### Others

You can create transformers for any schema validation library by implementing either `SchemaTransformerSync` or `SchemaTransformerAsync`:

```typescript
import type {
  SchemaTransformerSync,
  SchemaTransformerAsync,
} from '@ts-rest/open-api';

// Sync transformer example
const MY_SYNC_TRANSFORMER: SchemaTransformerSync = ({ schema }) => {
  // Check if this is your schema type
  if (isMySchemaType(schema)) {
    // Transform to OpenAPI schema
    return transform(schema);
  }

  // Return null if not your schema type
  return null;
};

// Async transformer example
const MY_ASYNC_TRANSFORMER: SchemaTransformerAsync = async ({ schema }) => {
  // Check if this is your schema type
  if (isMySchemaType(schema)) {
    // Transform to OpenAPI schema (async)
    return await transform(schema);
  }

  // Return null if not your schema type
  return null;
};
```

The transformer receives a `schema` parameter and should:

1. Check if the schema belongs to your validation library
2. If yes, transform it to an OpenAPI-compatible schema object
3. If no, return `null`

## Extending Operations with Additional OpenAPI Fields

We do not provide first-party support to set all possible OpenAPI fields on the operations such as the `security` field. In addition, you may have some specific needs to modify the fields already set by `ts-rest` such as the `tags` field.

Therefore, we have provided an `operationMapper` option to allow you to modify the OpenAPI fields of the operations. This is a callback function, that will receive the operation object, the contract endpoint, and the operation ID, and must return a valid OpenAPI operation object.
A common way to provide data to this function is to utilize the `metadata` field of the contract endpoint. However, feel free to come up with a different solution to doing this if you would not like to include this data in your contracts.

```typescript
const hasCustomTags = (
  metadata: unknown,
): metadata is { openApiTags: string[] } => {
  return (
    !!metadata && typeof metadata === 'object' && 'openApiTags' in metadata
  );
};

const hasSecurity = (
  metadata: unknown,
): metadata is { openApiSecurity: SecurityRequirementObject[] } => {
  return (
    !!metadata && typeof metadata === 'object' && 'openApiSecurity' in metadata
  );
};

const apiDoc = generateOpenApi(
  router,
  {
    info: { title: 'Blog API', version: '0.1' },
    components: {
      securitySchemes: {
        BasicAuth: {
          type: 'http',
          scheme: 'basic',
        },
      },
    },
  },
  {
    operationMapper: (operation, appRoute, id) => ({
      ...operation,
      ...(hasCustomTags(appRoute.metadata)
        ? {
            tags: appRoute.metadata.openApiTags,
          }
        : {}),
      ...(hasSecurity(appRoute.metadata)
        ? {
            security: appRoute.metadata.openApiSecurity,
          }
        : {}),
      // You can also use the operation ID for custom logic
      description: `${operation.description || ''} (Operation: ${id})`,
    }),
  },
);
```

## Enabling `operationId`s (Recommended!)

You can set `setOperationId` to either `true` or `concatenated-path` to set `operationId`s on your endpoints.

In the case of setting it to `true`, it will use only the endpoint name from your contract. You have to ensure that the endpoint names are unique across the entire contract.

In the case of setting it to `concatenated-path`, it will use the endpoint name concatenated with the path through the nested contract.
This is useful when you have multiple endpoints with the same name but different paths. This will result in longer but more descriptive `operationId`s.

```typescript
const openApiSchema = generateOpenApi(
  postsApi,
  {
    info: {
      title: 'Posts API',
      version: '1.0.0',
    },
  },
  {
    setOperationId: true, // [!code ++]
    // setOperationId: 'concatenated-path',
  },
);
```

Below is an example of what the OpenAPI document would look like with `operationId`'s enabled:

```typescript
{
  "openapi": "3.0.2",
  "paths": {
    "/posts": {
      "get": {
        "description": "Get all posts",
        "tags": [],
        "parameters": [
          {
            "name": "userId",
            "in": "query",
            "schema": {
              "type": "number"
            }
          }
        ],
        "operationId": "getPosts", // [!code ++]
        // or if using concatenated-path
        "operationId": "posts.getPosts", // [!code ++]
        "responses": {
```

## JSON Query Params

If you've enabled JSON Query params for your server and client, you can enable `jsonQuery` to mark the query params as `application/json` in the OpenAPI document:

```typescript
const openApiSchema = generateOpenApi(
  postsApi,
  {
    info: {
      title: 'Posts API',
      version: '1.0.0',
    },
  },
  {
    jsonQuery: true, // [!code ++]
  },
);
```

You'll want to do this to let your non ts-rest clients know that they should send the query params as JSON.
